﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using CNShapes;
using BobcatUtility;

namespace CNPageViewer
{
    /// <summary>
    /// This control shows shapes from a CNShapes.Page object.
    /// </summary><remarks>
    /// It observes the Page and shows the shapes it contains when the
    /// collection is updated.
    /// 
    /// This control may not show all the shapes in the collection. You can change
    /// the origin and the scale-factor, so you might be showing a subset of the
    /// shapes. These two parameters let you zoom in or our of the overall drawing.
    /// 
    /// registerRenderer
    /// ----------------
    /// This class uses classes that implement the IShapeRenderer interface to render the 
    /// different shape types. There should be one of these classes registered for each
    /// shape type that exists in the Page. These should be registered with this
    /// class in the registerRenderers() method by calling registerRenderer().
    /// </remarks>
    public partial class PageViewerControl : UserControl
    {
        #region Registration methods

        /// <summary>
        /// All renderers should be registered here.
        /// </summary>
        private void registerRenderers()
        {
            registerRenderer(new ShapeRenderer_Freehand());
            registerRenderer(new ShapeRenderer_Box());
            registerRenderer(new ShapeRenderer_Arrow());
            registerRenderer(new ShapeRenderer_Ellipse());
            registerRenderer(new ShapeRenderer_Pointer());
            registerRenderer(new ShapeRenderer_Text());
        }

        #endregion

        #region Public properties

        /// <summary>
        /// Property to get or set whether the control auto-zooms (i.e. the picture
        /// is scaled to fill the entire control).
        /// </summary>
        public bool AutoZoom
        {
            get { return m_bAutoZoom; }
            set { m_bAutoZoom = value; Invalidate(); }
        }

        /// <summary>
        /// Property to get or set the page object shown by this control.
        /// </summary>
        public Page Page
        {
            get
            {
                return m_Page;
            }
            set
            {
                // We unobserve events from any page we're currently showing...
                unobservePage();

                // We observe any future updates to the shapes and show the page...
                m_Page = value;
                observePage();
                Invalidate();
            }
        }

        /// <summary>
        /// Property to get and set the top-left corner of the view.
        /// </summary>
        public PointF Origin
        {
            get
            {
                return m_Origin;
            }
            set
            {
                m_Origin = Utility.copy(value);
                Invalidate();
            }
        }

        /// <summary>
        /// Property to get and set the zoom-factor.
        /// </summary>
        public float ZoomFactor
        {
            get
            {
                return m_fZoomFactor;
            }
            set
            {
                m_fZoomFactor = value;
                Invalidate();
            }
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Constructor.
        /// </summary>
        public PageViewerControl()
        {
            // We register the renderers for shapes we know about...
            registerRenderers();

            InitializeComponent();
        }

        /// <summary>
        /// Cleans up this control when it is no longer needed.
        /// </summary>
        public void close()
        {
            unobservePage();
        }

        /// <summary>
        /// Sets the origin and zoom factor to show the whole picture.
        /// </summary>
        public void sizeToContent()
        {
            calculateAutoZoomFactor();
            Invalidate();
        }

        /// <summary>
        /// Converts a point on the control into a point in the "shape space". This depends
        /// on the origin of the view and the zoom-factor.
        /// </summary>
        public PointF clientPointToShapePoint(PointF point)
        {
            PointF result = new PointF();
            result.X = (point.X / m_fZoomFactor) + m_Origin.X;
            result.Y = (point.Y / m_fZoomFactor) + m_Origin.Y;
            return result;
        }

        /// <summary>
        /// Converts a point in "shape-space" into a point on the control. This depends
        /// on the origin of the view and the zoom-factor.
        /// </summary>
        public PointF shapePointToClientPoint(PointF point)
        {
            PointF result = new PointF();
            result.X = (point.X - m_Origin.X) * m_fZoomFactor;
            result.Y = (point.Y - m_Origin.Y) * m_fZoomFactor;
            return result;
        }

        #endregion

        #region Private functions

        /// <summary>
        /// We stop observing events from the page we're showing.
        /// </summary>
        private void unobservePage()
        {
            if (m_Page == null)
            {
                return;
            }
            m_Page.ShapeAddedEvent -= onPageUpdated;
            m_Page.ShapeRemovedEvent -= onPageUpdated;
            m_Page.ShapeEditedEvent -= onPageUpdated;
            m_Page.ShapeEditCompletedEvent -= onPageUpdated;
        }

        /// <summary>
        /// We observe events from the page we're showing.
        /// </summary>
        private void observePage()
        {
            if (m_Page == null)
            {
                return;
            }
            m_Page.ShapeAddedEvent += onPageUpdated;
            m_Page.ShapeRemovedEvent += onPageUpdated;
            m_Page.ShapeEditedEvent += onPageUpdated;
            m_Page.ShapeEditCompletedEvent += onPageUpdated;
        }

        /// <summary>
        /// Called when the page has been updated. We may want to re-draw the shapes.
        /// </summary>
        private void onPageUpdated(object sender, Page.Args e)
        {
            // For the moment we just re-draw the entire set of shapes. Later we might
            // want to try to be more efficient and just re-draw a part of the image.
            // (The args from the Page contain a hint about which area of the
            // the collection has been updated.)
            Invalidate();
        }

        /// <summary>
        /// Calculates the zoom and origin required to fit the whole drawing into the control.
        /// </summary>
        private void calculateAutoZoomFactor()
        {
            // The page may not be set up yet...
            if (m_Page == null)
            {
                return;
            }

            // We find the size of the picture...
            Graphics graphics = CreateGraphics();
            RectangleF bounds = m_Page.getBounds(graphics);

            // We set a defualt zoom of 1 if there is nothing shown in the picture...
            if (bounds.Width == 0f || bounds.Height == 0f)
            {
                m_Origin.X = 0f;
                m_Origin.Y = 0f;
                m_fZoomFactor = 1f;
                return;
            }

            // The picture does have a size, so we scale acoordingly...
            m_Origin.X = bounds.X - bounds.Width * 0.05f;
            m_Origin.Y = bounds.Y - bounds.Height * 0.05f;

            float fXZoom = Width / bounds.Width;
            float fYZoom = Height / bounds.Height;
            m_fZoomFactor = Math.Min(fXZoom, fYZoom) * 0.90f;
        }

        /// <summary>
        /// The onPaint event. We draw the shapes from the Page.
        /// </summary>
        private void PageViewerControl_Paint(object sender, PaintEventArgs e)
        {
            // Has the Page been set up?
            if (m_Page == null)
            {
                return;
            }

            // We calcuate the auto-zoom factor (if auto-zoom is on)...
            if (m_bAutoZoom == true)
            {
                calculateAutoZoomFactor();
            }

            // We set up anti-aliasing...
            Graphics graphics = e.Graphics;
            graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

            // We draw the shapes...
            drawShapes(graphics);
        }

        /// <summary>
        /// We draw the shapes that fall into the visible area of the control.
        /// </summary>
        private void drawShapes(Graphics graphics)
        {
            RectangleF bounds = getBoundsInShapeCoordinates();
            IList<Shape> shapes = m_Page.getShapesForBounds(bounds, graphics);

            // We loop through these shapes, rendering them...
            foreach (Shape shape in shapes)
            {
                // We find the renderer for this shape and render it...
                Type shapeType = shape.GetType();
                if (m_mapTypesToRenderers.ContainsKey(shapeType) == false)
                {
                    // We just ignore any shape types that are not registered...
                    continue;
                }
                IShapeRenderer renderer = m_mapTypesToRenderers[shapeType];
                renderer.renderShape(shape, graphics, this);
            }
        }

        /// <summary>
        /// Returns the controls client area bounds translated into the coordinates used by the
        /// Page.
        /// </summary>
        private RectangleF getBoundsInShapeCoordinates()
        {
            // We translate the origin and the bottom-right point...
            PointF origin = clientPointToShapePoint(new PointF(0, 0));
            PointF bottomRight = clientPointToShapePoint(new PointF(ClientSize.Width, ClientSize.Height));

            // We put these into the results Rectangle...
            RectangleF result = new RectangleF();
            result.X = origin.X;
            result.Y = origin.Y;
            result.Width = bottomRight.X - origin.X;
            result.Height = bottomRight.Y - origin.Y;
            return result;
        }

        /// <summary>
        /// We register a renderer for one shape type.
        /// </summary>
        private void registerRenderer(IShapeRenderer renderer)
        {
            // We find the type of the shape that this renderer renders and add it to our map...
            Type renderedType = renderer.getShapeType();
            m_mapTypesToRenderers[renderedType] = renderer;
        }

        #endregion

        #region Private data

        // The Page that we show the shapes for...
        private Page m_Page = null;

        // The origin of the view in the "infinite canvaas" coordinates...
        private PointF m_Origin = new PointF { X = 0, Y = 0 };

        // If true, then the control auto-zooms so that the picture fills the control.
        private bool m_bAutoZoom = false;

        // The zoom-factor...
        private float m_fZoomFactor = 1;

        // Map of types to renderers. These should be set up in the constructor...
        private IDictionary<Type, IShapeRenderer> m_mapTypesToRenderers = new Dictionary<Type, IShapeRenderer>();

        #endregion

    }
}
